(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
componentDidMount是在第一次渲染後(安裝完畢)唯一觸發的生命週期函數。和前面一起來看的話,component安裝前到安裝後完整的生命週期就是:
constructor() -> static getDerivedStateFromProps() -> render() -> 渲染DOM -> componentDidMount()
咦? 我們不是已經把要渲染的東西丟到畫面上了嗎? 為什麼還要在這裡再做一個生命週期函數來用呢?以下是componentDidMount常被使用的狀況及理由。
前面提過,因為操作的元素在render()時元件還沒有真的渲染到DOM上,我們不能在render
函式操作和return元素有關的DOM。正確的作法是在componentDidMount
中操作。
像這樣的做法就會報錯:
import React, { Component } from 'react';
class Baby extends Component{
constructor(props) {
super(props);
this.state={
isRightDad: true
}
}
static getDerivedStateFromProps(props,state){
if(props.dad!=="Chang")
return {isRightDad:false}
}
render(){
if(this.state.isRightDad===true)
document.getElementById('msg').innerHTML="他的媽媽是小美";
else
document.getElementById('msg').innerHTML="他的媽媽,是誰,干你X事";
return(
<div id="msg">
讀取中
</div>
);
}
}
export default Baby;
必須要改成這樣
import React, { Component } from 'react';
class Baby extends Component{
constructor(props) {
super(props);
this.state={
isRightDad: true
}
}
static getDerivedStateFromProps(props,state){
if(props.dad!=="Chang")
return {isRightDad:false}
}
componentDidMount(){
if(this.state.isRightDad===true)
document.getElementById('msg').innerHTML="他的媽媽是小美";
else
document.getElementById('msg').innerHTML="他的媽媽,是誰,干你X事";
}
render(){
return(
<div id="msg">
讀取中
</div>
);
}
}
export default Baby;
另外,自己過去在查相關資料時,發現假使一定要用jQuery操作DOM,大部份的人也是建議在componentDidMount
中使用比較好,其中之一的原因也是因為這點。
前一篇提過,如果在渲染之前執行http request,元件不會等response進來才渲染,所以應該放在這裡來執行。這個時候會有個問題: 我們的資料還沒來,可是元素已經要渲染了,怎麼辦? 讓使用者看空白畫面?
請想想過去自己看過的網站,是不是常出現「在讀取完資料前會轉圈圈/顯示loading」這種網頁?
最常見的解決方法就是像這樣,透過用一個state在render
控制不同state要顯示的東西,讓這個state初始為false(顯示讀取提示),並在componentDidMount
執行http request,response進來後把該state改為true(顯示拿到的資料),就能做出剛剛所講的這種效果,也就是用UX去彌補這個問題。
在下面的範例,我們定義一個模擬ajax的函式ajaxSimulator()
,模擬用3秒從資料庫取得資料:
import React, { Component } from 'react';
class Baby extends Component{
constructor(props) {
super(props);
this.state={
isRightDad: true,
isGetData: false,
Mom: ""
}
this.ajaxSimulator=this.ajaxSimulator.bind(this)
}
ajaxSimulator(){
setTimeout(()=>{this.setState({isGetData:true, Mom:"小美"})},3000)
}
static getDerivedStateFromProps(props,state){
if(props.dad!=="Chang")
return {isRightDad:false}
}
componentDidMount(){
this.ajaxSimulator();
}
render(){
if(this.state.isRightDad===false)
return(
<div id="msg">你不是我爸</div>
);
else if(this.state.isGetData===false)
return(
<div id="msg">讀取中</div>
);
else
return(
<div id="msg">他的媽媽是{this.state.Mom}</div>
);
}
}
export default Baby;
一開始會顯示讀取中,3秒後當資料取得,會顯示該資料。
「東西」包含元素、監聽事件、setInterval...這個我們會在下一篇講componentWillUnmount
的時候一起講。
雖然css有animation和transition,但有的時候我們想做的動畫純css做不出來/只能透過js資料or操作DOM來產生(例如: 開場透過修改scrollTop做出scrollTo特效、使用別人提供的插件,而此插件只提供js上的api時)。這個時候也是要等渲染完成才能去觸發動畫條件。
下面這個範例就是在componentDidMount
呼叫自己寫的scrollTo函式,並透過setTimeout進行呼叫自己的recursive,以達成捲動動畫的效果。(其實就是前面講的 操作和「在render中return的元素」有關的DOM)
import React , {Component} from "react";
class Baby extends Component{
constructor(props) {
super(props);
this.state={
isRightDad: true,
isGetData: false,
Mom: ""
}
this.ajaxSimulator=this.ajaxSimulator.bind(this)
this.scrollTo=this.scrollTo.bind(this);
}
ajaxSimulator(){
setTimeout(()=>{this.setState({isGetData:true, Mom:"小美"})},3000)
}
scrollTo(){
/* 讀取 container元素的scrollLeft */
let scrollLeft=document.getElementById('container').scrollLeft;
if(scrollLeft<300){
/* 修改 container元素的scrollLeft */
document.getElementById('container').scrollLeft=scrollLeft+5;
setTimeout(this.scrollTo,20);
}
}
componentDidMount(){
this.ajaxSimulator();
this.scrollTo(); // 觸發開場動畫
}
render(){
return(
<div>
<div id="msg">
{(this.state.isGetData===false)?"讀取中":"他的媽媽是"+this.state.Mom}
</div>
<div id="container" style={{width:"400px",overflowX:"scroll"}}>
<div id="left" style={{width:"700px",fontSize:"30px",textAlign:"center"}}>
{"我是頭 O->---------------< 我是腳,我滑出來囉~ "}
</div>
</div>
</div>
)
}
}
export default Baby;
執行結果:
Mount只有元件安裝(第一次要渲染DOM時)會被觸發,也就是在component存在的期間,Mount系列的函數就只會被執行一次,其中componentDidMount
是所有生命週期函數中最常被使用到的。因為componentDidMount
在DOM渲染完成後觸發,我所查到的討論都是說如果你不確定要把一開始要觸發的東西放在哪個函數內,放在componentDidMount
大多數的時候都會是你最好的選擇。
講完了第一次渲染(安裝)會發生的事情,那麼第二次、第三次...後的渲染會發生什麼事呢?我們下一篇沒有要來講這個XD。
因為元件移除的生命週期函數componentWillUnmount
很常跟componentDidMount
一起使用,所以我們會先跳過更新的週期,直接來講移除。
請問componentDidMount()需要bind()嗎?
不需要喔!你可以在componentDidMount()內試著呼叫
componentDidMount() {
console.log(this);
}
理論上會印出你的React component
至於一般member function會需要bind
的原因是在綁定member function至UI互動event時(例如: button的onClick
),當event被觸發,負責呼叫該member function的不是你的React component本身,this
會指向undefined
另外我發現好像componentDidMount()會有被觸發多次的問題,例如以上面例子來說,我自己實驗就觸發兩次,而且不知道為何this.state.Mom印出來是空的,但畫面上有出現。
請問大大知道是怎麼回事嗎?
class Baby extends Component{
constructor(props) {
super(props);
this.state={
isGetData: false,
Mom: ""
}
this.ajaxSimulator=this.ajaxSimulator.bind(this)
this.scrollTo=this.scrollTo.bind(this);
}
ajaxSimulator(){
console.log("test")
setTimeout(()=>{
this.setState({isGetData:true, Mom:"小美"})
console.log(this.state.Mom)
},3000)
}
scrollTo(){
/* 讀取 container元素的scrollLeft */
let scrollLeft=document.getElementById('container').scrollLeft;
if(scrollLeft<300){
/* 修改 container元素的scrollLeft */
document.getElementById('container').scrollLeft=scrollLeft+5;
setTimeout(this.scrollTo,20);
}
}
componentDidMount(){
this.ajaxSimulator();
console.log("Did")
this.scrollTo(); // 觸發開場動畫
}
render(){
return(
<div>
<div id="msg">
{(this.state.isGetData===false)?"讀取中":"他的媽媽是"+this.state.Mom}
</div>
<div id="container" style={{width:"400px",overflowX:"scroll"}}>
<div id="left" style={{width:"700px",fontSize:"30px",textAlign:"center"}}>
{"我是頭 O->---------------< 我是腳,我滑出來囉~ "}
</div>
</div>
</div>
)
}
componentDidMount()會有被觸發多次的問題
這是2022年React 18推出後內建於開發者模式的功能,你在完成專案後打包成正式靜態檔案這個問題就會消失。這個功能用意是希望讓開發者能在開發時就檢查到是否有漏清除掉的effect(例如: addEventListener)。詳情請參考官方文件(layoutEffect === componentDidMount)
https://zh-hant.reactjs.org/blog/2022/03/29/react-v18.html#new-strict-mode-behaviors
官方建議的解決方式: https://beta.reactjs.org/learn/synchronizing-with-effects#how-to-handle-the-effect-firing-twice-in-development
為何this.state.Mom印出來是空
這個是我這篇文章的一個小錯誤,原因是React的setState
並不會在執行後馬上更新state的值。我後續有兩篇文章詳細解說原因和解法,請參考:
懂了,然來是因為非同步關係,謝謝!